1 /* 2 Copyright: Marcelo S. N. Mancini (Hipreme|MrcSnm), 2018 - 2021 3 License: [https://creativecommons.org/licenses/by/4.0/|CC BY-4.0 License]. 4 Authors: Marcelo S. N. Mancini 5 6 Copyright Marcelo S. N. Mancini 2018 - 2021. 7 Distributed under the CC BY-4.0 License. 8 (See accompanying file LICENSE.txt or copy at 9 https://creativecommons.org/licenses/by/4.0/ 10 */ 11 module hip.assets.textureatlas; 12 import hip.asset; 13 public import hip.api.data.textureatlas; 14 15 16 class HipTextureAtlas : HipAsset, IHipTextureAtlas 17 { 18 import hip.assets.image; 19 string atlasPath; 20 string[] texturePaths; 21 IHipTexture texture; 22 AtlasFrame[string] _frames; 23 24 ref AtlasFrame[string] frames(){return _frames;} 25 26 this() 27 { 28 super("TextureAtlas"); 29 _typeID = assetTypeID!HipTextureAtlas; 30 } 31 32 string getTexturePath () const 33 { 34 return texturePaths[0]; 35 } 36 37 38 bool loadTexture (in Image image) 39 { 40 import hip.assets.texture; 41 texture = new HipTexture(image); 42 if(!texture.hasSuccessfullyLoaded) 43 return false; 44 foreach(k, ref v; frames) 45 { 46 v.region = new HipTextureRegion(texture, 47 cast(uint)v.frame.x, cast(uint)v.frame.y, 48 cast(uint)(v.frame.x + v.frame.width), 49 cast(uint)(v.frame.y + v.frame.height) 50 ); 51 } 52 return true; 53 } 54 55 static HipTextureAtlas readJSON (ubyte[] data, string atlasPath, string texturePath) 56 { 57 import hip.data.json; 58 import hip.assets.texture; 59 HipTextureAtlas ret = new HipTextureAtlas(); 60 ret.texturePaths~= texturePath; 61 ret.atlasPath = atlasPath; 62 63 import hip.console.log; 64 65 JSONValue json = parseJSON(cast(string)data); 66 if(json["frames"].type == JSONType.array) 67 { 68 foreach(f; json["frames"].array) 69 { 70 AtlasFrame a; 71 a.filename = f["filename"].str; 72 a.rotated = f["rotated"].boolean; 73 a.trimmed = f["trimmed"].boolean; 74 JSONValue frameRect = f["frame"].object; 75 a.frame = AtlasRect( 76 cast(uint)frameRect["x"].integer, 77 cast(uint)frameRect["y"].integer, 78 cast(uint)frameRect["w"].integer, 79 cast(uint)frameRect["h"].integer 80 ); 81 frameRect = f["spriteSourceSize"].object; 82 a.spriteSourceSize = AtlasRect( 83 cast(uint)frameRect["x"].integer, 84 cast(uint)frameRect["y"].integer, 85 cast(uint)frameRect["w"].integer, 86 cast(uint)frameRect["h"].integer 87 ); 88 frameRect = f["sourceSize"].object; 89 a.sourceSize = AtlasSize(cast(uint)frameRect["w"].integer, cast(uint)frameRect["h"].integer); 90 ret.frames[a.filename] = a; 91 } 92 } 93 else 94 { 95 JSONValue frames = json["frames"].object; 96 JSONValue meta = json["meta"].object; 97 foreach(string frameName, JSONValue f; frames) 98 { 99 AtlasFrame a; 100 a.filename = frameName; 101 a.rotated = f["rotated"].boolean; 102 a.trimmed = f["trimmed"].boolean; 103 JSONValue frameRect = f["frame"].object; 104 a.frame = AtlasRect( 105 cast(uint)frameRect["x"].integer, 106 cast(uint)frameRect["y"].integer, 107 cast(uint)frameRect["w"].integer, 108 cast(uint)frameRect["h"].integer 109 ); 110 frameRect = f["spriteSourceSize"].object; 111 a.spriteSourceSize = AtlasRect( 112 cast(uint)frameRect["x"].integer, 113 cast(uint)frameRect["y"].integer, 114 cast(uint)frameRect["w"].integer, 115 cast(uint)frameRect["h"].integer 116 ); 117 frameRect = f["sourceSize"].object; 118 a.sourceSize = AtlasSize(cast(uint)frameRect["w"].integer, cast(uint)frameRect["h"].integer); 119 ret.frames[frameName] = a; 120 } 121 } 122 123 return ret; 124 } 125 126 /** 127 * If no texturePath is given, it will try spritesheetPath<.png> 128 * I found a txt file that is parsed as: 129 * 130 `spriteName = x y width height` 131 */ 132 static HipTextureAtlas readSpritesheet (string spritesheetPath, string texturePath = "") 133 { 134 import hip.filesystem.hipfs; 135 string data; 136 if(!HipFS.readText(spritesheetPath)) 137 { 138 import hip.error.handler; 139 ErrorHandler.showWarningMessage("Could not find spritesheet from path ", spritesheetPath); 140 return null; 141 } 142 import hip.util.path; 143 if(texturePath == "") 144 { 145 texturePath = spritesheetPath.dup.extension(".png"); 146 } 147 148 return readSpritesheet(cast(ubyte[])data, spritesheetPath, texturePath); 149 } 150 151 static HipTextureAtlas readSpritesheet (ubyte[] data, string spritesheetPath, string texturePath) 152 { 153 import hip.util.string:splitRange, trim, isNumber; 154 import hip.assets.texture; 155 156 string toParse = cast(string)data; 157 158 HipTextureAtlas atlas = new HipTextureAtlas(); 159 atlas.atlasPath = spritesheetPath; 160 atlas.texturePaths~= texturePath; 161 162 163 foreach(line; splitRange(toParse, "\n")) 164 { 165 import hip.util.algorithm; 166 import hip.util.conv; 167 import hip.console.log; 168 line = line.trim(); 169 auto lineDataRange = splitRange(line, " "); 170 lineDataRange.popFront; 171 string frame = lineDataRange.front; 172 if(frame != "") 173 { 174 while(!lineDataRange.empty && !lineDataRange.front.isNumber) 175 { 176 lineDataRange.popFront; //Find a number 177 } 178 int x = void, y = void, width = void, height = void; 179 lineDataRange.map((string data) => to!int(data)).put(&x, &y, &width, &height); 180 AtlasFrame atlasFrame; 181 atlasFrame.spriteSourceSize = AtlasRect(x, y, width, height); 182 atlasFrame.frame = AtlasRect(x, y, width, height); 183 atlasFrame.sourceSize = AtlasSize(width, height); 184 atlasFrame.filename = frame; 185 atlas.frames[frame] = atlasFrame; 186 } 187 } 188 return atlas; 189 } 190 191 192 static HipTextureAtlas readFromMemory (ubyte[] data, string atlasPath, string texturePath = ":IGNORE") 193 { 194 import hip.util.path; 195 switch(atlasPath.extension) 196 { 197 case "xml": 198 return HipTextureAtlas.readXML(data, atlasPath, texturePath); 199 case "atlas": 200 return HipTextureAtlas.readAtlas(data, atlasPath); 201 case "json": 202 return HipTextureAtlas.readJSON(data, atlasPath, texturePath == ":IGNORE" ? "" : texturePath); 203 default: 204 return HipTextureAtlas.readSpritesheet(data, atlasPath, texturePath == ":IGNORE" ? "" : texturePath); 205 } 206 } 207 208 /** 209 * Used for the following type of XML (Parsed without a real XML parser): 210 ```xml 211 <TextureAtlas imagePath="image.png"> 212 <SubTexture name="sub.png" x="0" y="0" width="0" height="0"/> 213 </TextureAtlas> 214 ``` 215 */ 216 static HipTextureAtlas readXML (ubyte[] data, string atlasPath, string texturePath = ":IGNORE") 217 { 218 import hip.assets.texture; 219 import hip.util.string; 220 import hip.util.path; 221 import hip.util.conv; 222 string dataToParse = cast(string)data; 223 import hip.console.log; 224 //TODO: Fix .after (as it only executes startsWith and is returning null) 225 dataToParse = dataToParse.findAfter("imagePath="); 226 if(texturePath == ":IGNORE") 227 texturePath = atlasPath.dup.extension(".png"); 228 else 229 texturePath = dataToParse.between("\"", "\""); 230 HipTextureAtlas atlas = new HipTextureAtlas(); 231 atlas.texturePaths~= texturePath; 232 dataToParse = dataToParse.findAfter(">"); 233 234 foreach(line; dataToParse.splitRange("\n")) 235 { 236 line = line.trim(); 237 if(!line) 238 continue; 239 if(line.startsWith("</TextureAtlas>")) 240 break; 241 string name = (line = line.findAfter("name=")).between(`"`, `"`); 242 int x = (line = line.findAfter("x=")).between(`"`, `"`).to!int; 243 int y = (line = line.findAfter("y=")).between(`"`, `"`).to!int; 244 int width = (line = line.findAfter("width=")).between(`"`, `"`).to!int; 245 int height = (line = line.findAfter("height=")).between(`"`, `"`).to!int; 246 247 AtlasFrame frame; 248 frame.filename = name; 249 frame.frame = AtlasRect(x, y, width, height); 250 frame.spriteSourceSize = AtlasRect(x, y, width, height); 251 frame.sourceSize = AtlasSize(width, height); 252 atlas.frames[frame.filename] = frame; 253 } 254 return atlas; 255 } 256 257 258 static HipTextureAtlas readAtlas (ubyte[] data, string atlasPath) 259 { 260 import hip.util.string : split, countUntil; 261 import hip.util.conv : to; 262 263 HipTextureAtlas ret = new HipTextureAtlas(); 264 ret.atlasPath = atlasPath; 265 string atlasFile = cast(string)data; 266 267 string[] lines = atlasFile.split("\n"); 268 int i = 0; 269 while(lines[i] == "") 270 i++; 271 string textureName = lines[i++]; 272 ret.texturePaths~= textureName; 273 string sizeText = lines[i++]; 274 string format = lines[i++]; 275 string filter = lines[i++]; 276 string repeat = lines[i++]; 277 278 const int offset = i; 279 280 for(; i < lines.length-offset; i+= 7) 281 { 282 AtlasFrame frame; 283 frame.trimmed = false; 284 frame.filename = lines[i]; 285 286 string rotate = lines[i+1]; 287 rotate = rotate[rotate.countUntil(":")+2 .. $]; 288 frame.rotated = to!bool(rotate); 289 290 string xy = lines[i+2]; 291 xy = xy[xy.countUntil(":")+2 .. $]; 292 293 ptrdiff_t commaIndex = xy.countUntil(','); 294 int x = to!int(xy[0..commaIndex]); 295 //To account space must increate 2 296 int y = to!int(xy[commaIndex+2..$]); 297 298 string size = lines[i+3]; 299 size = size[size.countUntil(":")+2 .. $]; 300 301 commaIndex = size.countUntil(','); 302 int sizeW = to!int(size[0..commaIndex]); 303 int sizeH = to!int(size[commaIndex+2..$]); 304 305 string orig = lines[i+4]; 306 orig = orig[orig.countUntil(":")+2 .. $]; 307 308 commaIndex = orig.countUntil(','); 309 int origX = to!int(orig[0..commaIndex]); 310 int origY = to!int(orig[commaIndex+2..$]); 311 312 string _offset = lines[i+5]; 313 _offset = _offset[_offset.countUntil(":")+2 .. $]; 314 315 commaIndex = _offset.countUntil(','); 316 int _offsetX = to!int(_offset[0..commaIndex]); 317 int _offsetY = to!int(_offset[commaIndex+2..$]); 318 319 string index = lines[i+6]; 320 index = index[index.countUntil(":")+2 .. $]; 321 322 frame.frame = AtlasRect(x, y, sizeW, sizeH); 323 frame.spriteSourceSize = AtlasRect(_offsetX, _offsetY, sizeW, sizeH); 324 frame.sourceSize = AtlasSize(sizeW, sizeH); 325 ret.frames[frame.filename] = frame; 326 } 327 return ret; 328 } 329 330 331 override void onFinishLoading(){} 332 override void onDispose(){} 333 bool isReady(){return texture !is null && frames.length > 0;} 334 335 336 alias frames this; 337 }